﻿using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Caching;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace EpubFactory.Common
{
    public class EbookException : ApplicationException
    {
        public EbookException() { }
        public EbookException(string message) : base(message) { }
        public EbookException(string message, Exception innerException) : base(message, innerException) { }
    }

    public class ChapterEventArgs : EventArgs
    {
        public Chapter Chapter { get; set; }
        public ChapterEventArgs(Chapter chapter)
        {
            this.Chapter = chapter;
        }
    }
    public class BookEventArgs : EventArgs
    {
        public Book Book { get; set; }
        public BookEventArgs(Book book)
        {
            this.Book = book;
        }
    }
    public static class StringExtension
    {
        public static string GenerateSlug(this string phrase)
        {
            string str = phrase.RemoveAccent().ToLower();
            // invalid chars           
            str = Regex.Replace(str, @"[^a-z0-9\s-]", "");
            // convert multiple spaces into one space   
            str = Regex.Replace(str, @"\s+", " ").Trim();
            // cut and trim 
            str = str.Substring(0, str.Length <= 45 ? str.Length : 45).Trim();
            str = Regex.Replace(str, @"\s", "-"); // hyphens   
            return str;
        }

        public static string RemoveAccent(this string txt)
        {
            byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
            return System.Text.Encoding.ASCII.GetString(bytes);
        }
    }
    public abstract class Receipe
    {
        Regex html4EncodingRegex = new Regex(@"<meta([\s\S]+?)>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        Regex html5EncodingRegex = new Regex(@"<meta charset=""([\w\d-]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        Regex encodingRegex = new Regex(@"charset=""?([\w\d-]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly ObjectCache cache = MemoryCache.Default;
        protected static readonly Logger log = LogManager.GetLogger("receipe", typeof(Receipe));
        protected const string PC_USER_AGENT = "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0";
        protected const string MOB_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Version/10.0 Mobile/14D27 Safari/602.1";
        public event EventHandler<BookEventArgs> BookSummaryCompleted;
        public EventHandler<BookEventArgs> BookSummaryOk { get; set; }
        public EventHandler<ChapterEventArgs> ChapterOk { get; set; }
        public event EventHandler<ChapterEventArgs> ChapterContentLoad;
        public event EventHandler<ChapterEventArgs> ChapterContentCleaned;
        public event EventHandler<ChapterEventArgs> ChapterImagesLoad;
        public virtual string TempDir { get; set; }
        protected string entrypoint;
        protected Func<HttpClient> httpFactory;

        protected HttpClient http;
        public abstract IEnumerable<Uri> SourceUriList { get; }
        public abstract string Name { get; }
        public virtual string ReadmeHtml { get; private set; }
        public virtual Encoding SiteEncoding { get { return Encoding.UTF8; } }
        public virtual string DefaultReadme()
        {
            using (var stream = (Assembly.GetExecutingAssembly()).GetManifestResourceStream("EpubFactory.Common.readme.md"))
            {
                using (var reader = new StreamReader(stream))
                {
                    var txt = reader.ReadToEnd();
                    string urlList = null;
                    if (SourceUriList != null)
                    {
                        urlList = string.Join(",", SourceUriList);
                    }
                    else
                    {
                        urlList = string.Empty;
                    }
                    return string.Format(txt, urlList);
                }
            }
        }

        protected Book GetEmptyBook()
        {
            var book = new Book()
            {
                Chapters = new List<Chapter>(),
                Images = new List<string>(),
                Authors = new List<string>()
            };
            return book;
        }

        public Receipe(Func<HttpClient> httpFactory = null)
        {
            if (httpFactory == null)
            {
                this.httpFactory = () =>
                {
                    var client = new HttpClient(new HttpClientHandler()
                    {
                        AllowAutoRedirect = false
                    });
                    client.DefaultRequestHeaders.Add("UserAgent", PC_USER_AGENT);
                    //client.DefaultRequestHeaders.Add("Referer", host);
                    return client;
                };
            }
            else
            {
                this.httpFactory = httpFactory;
            }
            this.http = this.httpFactory.Invoke();
        }
        public Encoding CheckHtmlEncoding(string html)
        {
            Encoding encoding = null;
            foreach (Match match in encodingRegex.Matches(html))
            {
                if (match.Groups.Count >= 2 && !string.IsNullOrWhiteSpace(match.Groups[1].Value))
                {
                    encoding = Encoding.GetEncoding(match.Groups[1].Value);
                    break;
                }
            }
            if (encoding == null)
            {
                encoding = Encoding.GetEncoding("gbk");
            }
            return encoding;
        }
        public string ConvertToUtf8(string html)
        {
            var encoding = CheckHtmlEncoding(html);
            return ConvertToUtf8(html, encoding);
        }
        public string ConvertToUtf8(string html, Encoding srcEncoding)
        {
            var bytes = srcEncoding.GetBytes(html);
            var bytes2 = Encoding.Convert(srcEncoding, Encoding.UTF8, bytes);
            var result = Encoding.UTF8.GetString(bytes2);
            return result;
        }
        public virtual string GetCachedContent(string url)
        {
            return GetCachedContent(url, SiteEncoding);
        }
        public virtual string GetCachedContent(string url, Encoding encoding)
        {
            string html = null;
            var enableCache = AppPreference.Load().EnableRequestCache;
            if (enableCache)
            {
                html = (string)cache.Get(url);
            }
            if (string.IsNullOrWhiteSpace(html))
            {
                html = GetContent(url);
                if (enableCache)
                {
                    cache.Add(url, html, DateTimeOffset.Now.AddDays(1));
                }
            }
            return html;
        }
        public virtual string GetContent(string url)
        {
            return GetContent(url, SiteEncoding);
        }
        public virtual string GetCachedContent(string url, CancellationToken token)
        {
            string html = null;
            var enableCache = AppPreference.Load().EnableRequestCache;
            if (enableCache)
            {
                html = (string)cache.Get(url);
            }
            if (string.IsNullOrWhiteSpace(html))
            {
                var task = http.GetAsync(url, token).ContinueWith((res) =>
                {
                    if (SiteEncoding == Encoding.UTF8)
                    {
                        res.Result.Content.ReadAsStringAsync().ContinueWith((content) =>
                        {
                            html = content.Result;
                            if (enableCache)
                            {
                                cache.Add(url, html, DateTimeOffset.Now.AddDays(1));
                            }
                        });
                    }
                    else
                    {
                        res.Result.Content.ReadAsByteArrayAsync().ContinueWith((bytes) =>
                        {
                            var bytes2 = Encoding.Convert(SiteEncoding, Encoding.UTF8, bytes.Result);
                            html = Encoding.UTF8.GetString(bytes2);
                            if (enableCache)
                            {
                                cache.Add(url, html, DateTimeOffset.Now.AddDays(1));
                            }
                        });
                    }

                });
                try
                {
                    task.Wait();
                }
                catch (AggregateException ex)
                {
                    throw new EbookException("获取页面内容出错", ex);
                }
            }
            return html;
        }
        public virtual string GetContent(string url, Encoding encoding)
        {
            log.Debug("GET {0}", url);
            string content = null;
            var res = http.GetAsync(url).Result;
            log.Debug("GET {0} {1}", url, res.StatusCode);
            res.EnsureSuccessStatusCode();
            if (encoding != Encoding.UTF8)
            {
                var bytes = res.Content.ReadAsByteArrayAsync().Result;
                var bytes2 = Encoding.Convert(encoding, Encoding.UTF8, bytes);
                content = Encoding.UTF8.GetString(bytes2);
            }
            else
            {
                content = res.Content.ReadAsStringAsync().Result;
            }

            return content;
        }
        public virtual Book Execute(string entrypoint, CancellationTokenSource cts = null)
        {
            this.entrypoint = entrypoint;
            var book = GetBook();
            if (book.Images == null)
            {
                book.Images = new List<string>();
            }
            var handler = BookSummaryCompleted;
            if (handler != null)
            {
                handler(this, new BookEventArgs(book));
            }
            if (BookSummaryOk != null)
            {
                BookSummaryOk(this, new BookEventArgs(book));
            }

            var chapters = book.FlattedChapters;
            var option = new ParallelOptions();
            if (cts != null)
            {
                option.CancellationToken = cts.Token;
            }
            if (AppPreference.Load().ParallelismDegree > 0)
            {
                option.MaxDegreeOfParallelism = AppPreference.Load().ParallelismDegree;
            }
            List<Task> tasks = new List<Task>();

            //foreach (var chapter in chapters)
            //{

            //}

            Parallel.ForEach(chapters, option, (chapter, state) =>
            {
                if (chapter.Book == null)
                {
                    chapter.Book = book;
                }
                if (!string.IsNullOrWhiteSpace(chapter.Href))
                {
                    var task = GetChapter(chapter, cts).ContinueWith((mm) =>
                    {
                        CleanChapter(chapter);
                        var handler3 = ChapterContentCleaned;
                        if (handler3 != null)
                        {
                            handler3(this, new ChapterEventArgs(chapter));
                        }
                        GetImages(chapter);
                        var handler4 = ChapterImagesLoad;
                        if (handler4 != null)
                        {
                            handler4(this, new ChapterEventArgs(chapter));
                        }

                        if (ChapterOk != null)
                        {
                            ChapterOk(this, new ChapterEventArgs(chapter));
                        }
                    });
                    if (task != null)
                    {
                        tasks.Add(task);
                    }
                }
                else
                {
                    HandleEmptyChapter(chapter);
                }
            });
            if (tasks.Any())
            {
                try
                {
                    Task.WaitAll(tasks.Where(g => g != null).ToArray(), cts.Token);
                }
                catch (TaskCanceledException ex)
                {
                    log.Error(ex);
                }
                catch (AggregateException ex)
                {
                    //if (ex.InnerException != null && ex.InnerException is TaskCanceledException)
                    //{

                    //}
                    log.Error(ex);
                    throw new EbookException("解析出错", ex);
                }
                catch (HttpRequestException ex)
                {
                    log.Error(ex);
                }
            }
            return book;
        }
        public virtual void HandleEmptyChapter(Chapter chapter)
        {
            var handler2 = ChapterContentLoad;
            if (handler2 != null)
            {
                handler2(this, new ChapterEventArgs(chapter));
            }
            chapter.SourceContent = string.Empty;
            chapter.Content = string.Empty;

            var handler3 = ChapterContentCleaned;
            if (handler3 != null)
            {
                handler3(this, new ChapterEventArgs(chapter));
            }
            var handler4 = ChapterImagesLoad;
            if (handler4 != null)
            {
                handler4(this, new ChapterEventArgs(chapter));
            }
            if (ChapterOk != null)
            {
                ChapterOk(this, new ChapterEventArgs(chapter));
            }
        }
        public abstract Book GetBook();
        public virtual Task GetChapter(Chapter chapter, CancellationTokenSource cts)
        {
            if (cts == null)
            {
                return GetChapter(chapter);
            }
            var html = GetCachedContent(chapter.Href, cts.Token);
            var handler2 = ChapterContentLoad;
            if (handler2 != null)
            {
                handler2(this, new ChapterEventArgs(chapter));
            }
            chapter.SourceContent = html;
            return Task.FromResult<Chapter>(chapter);
        }
        public virtual Task GetChapter(Chapter chapter)
        {
            var html = GetCachedContent(chapter.Href);
            chapter.SourceContent = html;
            return Task.FromResult(chapter);
        }
        public string GetBookPath(Book book)
        {
            return GetBookPath(book.EPubId, book.Host);
        }
        public virtual string ExtractHost(string url)
        {
            var uri = (new Uri(url));
            return string.Format("{0}://{1}", uri.Scheme, uri.Host);
        }
        public string GetBookPath(string bookId, string host)
        {
            var slug = host.GenerateSlug();
            var basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            var path = Path.Combine(basePath, slug, bookId);
            return path;
        }
        public abstract string ExtractBookId(string url);

        public virtual void CleanChapter(Chapter chapter)
        {
            var html = chapter.SourceContent;
            if (string.IsNullOrWhiteSpace(html))
            {
                chapter.SourceContent = GetContent(chapter.Href);
            }
            html = chapter.SourceContent;
            if (string.IsNullOrWhiteSpace(html))
            {
                log.Error("cannot find html {0}", chapter.Href);
                return;
            }
        }
        public virtual void GetImages(Chapter chapter)
        {

        }

        public void Download(string url, string path, bool overwrite = false)
        {
            if (File.Exists(path))
            {
                if (overwrite)
                {
                    File.Delete(path);
                }
                else
                {
                    log.Warn("Skip {0} because target exist", url);
                    return;
                }
            }
            log.Debug("GET {0}", url);

            var res = http.GetAsync(url).Result;
            log.Debug("GET {0} {1}", url, res.StatusCode);
            res.EnsureSuccessStatusCode();
            using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                try
                {
                    var task = res.Content.CopyToAsync(fileStream);
                    task.Wait();
                }
                finally
                {
                    fileStream.Close();
                }
            }
        }
    }
}
